#!/usr/bin/env python
# -*- coding: ascii -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
#
# opensearch_suggest_cherrypy.py - Demo Open Search 1.1 server implementation and Javascript client implementation 
# Copyright (C) 2010  Chris Clark
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Open Search 1.1 server implementation and Javascript client implementation 

Useful for Web Browsers that support Open Search 1.1 plugins (Firefox, IE, etc.)

Also provides a Javascript (JQuery based) client to Open Search 1.1 servers

opensearch_suggest_cherrypy.py

        See 
            https://developer.mozilla.org/en/Creating_OpenSearch_plugins_for_Firefox
            https://developer.mozilla.org/en/Supporting_search_suggestions_in_search_plugins
        also see google.xml in firefox settings/opensearch plugin dir
        
        http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.0
        http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1/Draft_1
        
        
        ideal would be a javascript client that you can point at the opensearch xml file and it will work out the suggestion url (scheme)
        
        won't work could be hacked:
            http://woork.blogspot.com/2008/02/search-with-autosuggest-feature.html -- would need lots of hacking

        ojax http://ojax.sourceforge.net/demo.html appeasrs to handle json form that opensarch uses but is wierd (not documented, appeasr to need custom code)
        


        Scriptaculous Autocomplete with JSON and IE6 support
        The script.aculo.us Autocomplete control actually takes in an HTML partial 
        rather than a proper JSON or XML data format. 
        Furthermore, the keyboard navigation of the autocomplete widget fails in IE6.
        http://lebensold.net/creative-stuff/scriptaculous-autocomplete-with-json-and-ie6-support


        
        
        
        
        http://ex-libris.ca/?p=694
        http://search.cambridgelibraries.ca:2082/search
        http://www2.cambridgelibraries.ca/mike/yaz/opacsuggest.php?callback=jsonp1243802404633&_=1243802413645&q=can&limit=10&timestamp=1243802413644&searchtype=t
            jsonp1243802404633(["can a busy christian develop her spiritual life answers to questions women ask about spirituality", "can a giraffe cry or laugh", "can adults become human", "can amera parkway franklin boulevard to townline road pinebush road addendum to class environmental assessment", "can amera parkway franklin boulevard to townline road pinebush road class environmental assessment environmental study report", "can anybody hear me", "can asians think", "can diets be harmful", "can glance online", "can i change the way i look a teens guide to the health implications of cosmetic surgery makeovers and beyond"])
            
        appesrs to use jQuery plugin: Autocomplete
            
        wrong json format:
        jQuery plugin: Autocomplete
        http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
        http://jquery.bassistance.de/autocomplete/demo/
        http://jquery.bassistance.de/autocomplete/demo/emails.php?q=pe
            [{ name: "Peter Pan", to: "peter@pan.de" }, ]
        
        json
        http://jquery.bassistance.de/autocomplete/demo/json.html
        http://jquery.bassistance.de/autocomplete/demo/emails.php?q=a&limit=10&timestamp=1243803530512
            [{ name: "Peter Pan", to: "peter@pan.de" }, { name: "Forneria Marconi", to: "live@japan.jp" }, { name: "Master Sync", to: "205bw@samsung.com" }, { name: "Donnie Darko", to: "dd@timeshift.info" }, { name: "Quake The Net", to: "webmaster@quakenet.org" }, ]
        
            
            
        http://blog.schuager.com/2008/09/jquery-autocomplete-json-apsnet-mvc.html
        http://www.it-eye.nl/weblog/2008/08/23/using-jquery-autocomplete-with-grails-and-json/
        http://farazt.blogspot.com/2008/10/autocomplete-using-jquery-mvc-and-json.html
        
        
        AutoComplete 1.2 Scriptaculous prototype with json
        http://www.cs.princeton.edu/~lbai/LF/ryan/doc/examples.html
            ["\r\nreabbreviate","reabbreviated","reabbreviates","reabbreviating","reach","reachability","reachable","reachably","reached","reacher","reaches","reaching","reacquired","react","reacted","reacting","reaction","reactionaries","reactionary","reactions","reactivate","reactivated","reactivates","reactivating","reactivation","reactive","reactively","reactivity","reactor","reactors","reacts","read","readability","readable","reader","readers","readied","readier","readies","readiest","readily","readiness","reading","readings","readjusted","readout","readouts","reads","ready","readying","Reagan","real","realest","realign","realigned","realigning","realigns","realism","realist","realistic","realistically","realists","realities","reality","realizable","realizably","realization","realizations","realize","realized","realizes","realizing","reallocate","really","realm","realms","realness","reals","realtor","ream","reanalyze","reanalyzes","reanalyzing","reap","reaped","reaper","reaping","reappear","reappeared","reappearing","reappears","reappraisal","reappraisals","reaps","rear","reared","rearing","rearrange","rearrangeable","rearranged","rearrangement","rearrangements","rearranges","rearranging","rearrest","rearrested","rears","reason","reasonable","reasonableness","reasonably","reasoned","reasoner","reasoning","reasonings","reasons","reassemble","reassembled","reassembles","reassembling","reassembly","reassessment","reassessments","reassign","reassigned","reassigning","reassignment","reassignments","reassigns","reassure","reassured","reassures","reassuring","reawaken","reawakened","reawakening","reawakens"]
        
        
        http://www.developer.com/design/article.php/10925_3719341_1
        http://www.developer.com/design/article.php/10925_3719341_4

"""

import os
import sys
import tempfile
import locale
import time
import re
import cgi
import urllib
import textwrap

try:
    # in python 2.6
    import json
except ImportError:
    try:
        # from http://code.google.com/p/simplejson
        import simplejson as json
    except ImportError:
        json = None
if json is None:
    raise ImportError()
    # hacks below won't work robustly (or safely as Python code could be executed)
    
    def dump_json(x):
        """dumb not strict/compliant!"""
        return repr(x)
        
    def read_json(x):
        """dumb not safe!"""
        import warnings
        warnings.warn('about to evaluate/execute potentially unsafe code (read_json)')
        return eval(x)
else:
    dump_json = json.dumps
    read_json = json.loads


#import cherrypy # http://www.cherrypy.org/
## rhubarb tart # http://rhubarbtart.org/
## PySite (not Pysite, note case) 
##      http://www.programmingforums.org/forum/f43-python/t9028-pysite-web-development-framework.html
##      http://www.programmingforums.org/thread9028.html
try:
    #raise(ImportError)
    import cherrypy
    from cherrypy.lib.static import serve_file
except ImportError, info:
    import dietcherrypy as cherrypy
    serve_file = cherrypy.serve_file

try:
    #raise ImportError()
    import ftsearch_whoosh as ftsearch
except ImportError:
    print 'defaulting to fake search module'
    #raise
    import ftsearch


current_dir = os.path.dirname(os.path.abspath(__file__))


class Root(object):
    def __init__(self, textfile_path=None):
        textfile_path = textfile_path or 'aesop'
        self.fts_handle = ftsearch.setup(textfile_path)
        print self.fts_handle
        f = open('index.html')
        self.html_string = f.read()
        f.close()
    
    def index(self, search_term=None):
        """Simple web search (this is a template you should fill your own implementation here
        """
        if search_term:
            search_term = search_term.strip()  # remove white space
            search_term = search_term.decode('utf8')  # guess encoding. Very simplistic
            return ftsearch.search(self.fts_handle, search_term)
        return self.html_string
    index.exposed = True

    def opensearch_xml(self):
        """OpenSearch download URL for /opensearch.xml
        """
        cherrypy.response.headers['Content-Type'] = 'application/opensearchdescription+xml'
        
        # NOTE ShortName and Description should match in some way the title in the main document link to this xml document
        xml = '''<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
                       xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>OpenSearchDemo</ShortName>
<Description>OpenSearchDemo Search</Description>
<InputEncoding>UTF-8</InputEncoding>
<Url type="application/opensearchdescription+xml"
     rel="self"
     template="%s" />
<Url type="application/x-suggestions+json" method="GET" template="%s">
  <Param name="search_term" value="{searchTerms}"/>
  <Param name="maxnum" value="9"/>
</Url>
<Url type="text/html" method="GET" template="%s">
  <Param name="search_term" value="{searchTerms}"/>
</Url>
<moz:SearchForm>%s</moz:SearchForm>
</OpenSearchDescription>
''' % (cherrypy.url('opensearch.xml'), cherrypy.url('suggest'), cherrypy.url('/'), cherrypy.url('/'))
        return xml
    opensearch_xml.exposed = True
    
    def suggest(self, search_term, maxnum=5):
        """Return OpenSearch suggestion. in json
        
        http://localhost:8080/suggest?search_term=fi&maxnum=9
        http://localhost:8080/?search_term=more

        See 
            https://developer.mozilla.org/en/Creating_OpenSearch_plugins_for_Firefox
            https://developer.mozilla.org/en/Supporting_search_suggestions_in_search_plugins
        also see google.xml in firefox settings dir.
        
        http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.0
        http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1/Draft_1
        """
        
        maxnum = int(maxnum)
        search_term = search_term.strip()  # remove white space
        orig_search_term_list = search_term.strip().split()
        search_term = orig_search_term_list.pop()  # remove last word/search term (can not do phrase lookups) and use that for lookup
        orig_search_term = ''.join(orig_search_term_list) + ' '
        
        result_list = []
        result_list_desc = []
        for tmpterm in ftsearch.gen_find_top_search_terms(self.fts_handle, search_term, maxnum):
            result_list.append(orig_search_term + tmpterm[1])  # could use count for Descriptions fields, FireFox does not support this though (probably useful for search field with unobtrusive javascript helpers)
            result_list_desc.append('%d results' % tmpterm[0])  # TODO make number have commas for seperators for easy reading
        result_dict = [search_term, result_list, result_list_desc]
        #result_dict=[search_term, result_list] ## DEBUG DO NOT USE, test missing (optional) description
        return dump_json(result_dict)
    suggest.exposed = True
    
    def jquery_suggest(self, q, limit=None, timestamp=None):
        """autocompete with api/params that 
            http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/ 
        expects but with opensearch json result set/type/format.
        
        Basically has a slightly different API signature to the OpenSearch specification but simply wraps self.suggest() and so returns the same data/structure
        """
        result = self.suggest(q, limit)
        ##TODO set content type (you would think json but google sends javascript) and encoding (utf8)
        return result
    jquery_suggest.exposed = True


class JQuery(object):
    """Simplistic "expose these JQuery files" (could just expose the whole directory)
    """
    @cherrypy.expose
    def jquery_autocomplete_js(self):
        local_filename = 'jquery.autocomplete.js'
        return serve_file(os.path.join(current_dir, 'jquery', local_filename),
                                  content_type='application/javascript')
    
    @cherrypy.expose
    def jquery_autocomplete_css(self):
        local_filename = 'jquery.autocomplete.css'
        return serve_file(os.path.join(current_dir, 'jquery', local_filename),
                                  content_type='text/css')
    
    @cherrypy.expose
    def indicator_gif(self):
        local_filename = 'indicator.gif'
        return serve_file(os.path.join(current_dir, 'jquery', local_filename),
                                  content_type='image/gif')
    
    @cherrypy.expose
    def jquery_ajaxQueue_js(self):
        local_filename = 'jquery.ajaxQueue.js'
        return serve_file(os.path.join(current_dir, 'jquery', local_filename),
                                  content_type='application/javascript')
    
    @cherrypy.expose
    def jquery_bgiframe_min_js(self):
        local_filename = 'jquery.bgiframe.min.js'
        return serve_file(os.path.join(current_dir, 'jquery', local_filename),
                                  content_type='application/javascript')
    
    @cherrypy.expose
    def jquery_js(self):
        local_filename = 'jquery.js'
        return serve_file(os.path.join(current_dir, 'jquery', local_filename),
                                  content_type='application/javascript')
    

def main(argv=None):
    if argv is None:
        argv = sys.argv
    
    print 'cherrypy.__version__', cherrypy.__version__
    
    server_port = 8080
    no_webbrowser = False  # pick up from command line?
    JYTHON_RUNTIME_DETECTED = 'java' in sys.platform.lower() or hasattr(sys, 'JYTHON_JAR') or str(copyright).find('Jython') > 0
    DOTNET_MONO_CLR_DETECTED = sys.platform == 'cli'
    if JYTHON_RUNTIME_DETECTED or DOTNET_MONO_CLR_DETECTED:
        no_webbrowser = True
    
    server_name = '127.0.0.1'  # Make this 0.0.0.0 to allow any external client
    
    """
    ## cherrypy v3 quickstart (no call backs allowed, need to thread locally)
    #cherrypy.quickstart(Root(form, self._webform_callback))
    
    ### cherrypy 3.0.2 does NOT have server.start_with_callback it is engine..?
    cherrypy.config.update({'server.socketPort':server_port})
    cherrypy.engine.start_with_callback(webbrowser.open, ('http://localhost:%d'%server_port,))
    """
    ### cherrypy 2.?.? (and dietcherypy)
    #cherrypy.config.update({'server.socketPort':server_port}) # maybe a 3.0 thing?
    cherrypy.config.update({'server.socket_port': server_port, 'server.socket_host': server_name})  # CherryPy 3.1.2
    if DOTNET_MONO_CLR_DETECTED:
        cherrypy.config.update({'server.socket_host': server_name})  # CherryPy 3.1.2
    mywebapp = Root()
    mywebapp.jquery = JQuery()
    if no_webbrowser:
        cherrypy.root = mywebapp
        cherrypy.server.start()
    else:
        # style start for:
        #   CherryPy 3.1 (tested with 3.1.2)
        #   dietcherypy
        
        def launch_webbrowser():
            import webbrowser

            url = 'http://localhost:%d' % server_port
            webbrowser.open(url)
        cherrypy.engine.subscribe('start', launch_webbrowser)  # CherryPy 3.1 api
        cherrypy.quickstart(mywebapp)

    return 0


if __name__ == "__main__":
    sys.exit(main())
